#include #include /* * The general concept of JDK 8+'s ThreadLocalRandom's nextInt is as follows * Obtain a seed from the Thread t as a long, r. * Increment r by the predefined value GAMMA mod 2^64, and return r. * Pass r to the mix32 function mix32(long z) *************************************************** * Procedure mix32(long z) * z = (z ^ (z >> 33)) * 0xff51afd7ed558ccd mod 2^64 * z = (z ^ (z >> 33)) * 0xc4ceb9fe1a85ec53 mod 2^64 * return (int32)(z >> 32); *************************************************** * For the proof of concept, we use unsigned integers for consistency. * This can be understood as returning the high bits of a 64 bit integer. * Lossily removing the 32 LSB. * To uniquely proceed with the attack, we require 3 outputs. To obtain very few, * likely one, collision, we only require 2. * After collecting three outputs, we place the first output into the high bits of * a 64 bit numebr. We then iterate through all 2^32 possible low bits by * concatenating them to these high bits. This allows us to get past the lossy * compression down to a 32 bit integer. Then, we note that both constants are * odd, and thus unitary on the ring Z/2^64Z, implying the existence of a * multiplicative inverse. We first enumerate and apply the inverse of: * 0xc4ceb9fe1a85ec53 * At this point, we notice that our guess necessarily must have the 33 high bits * of the previous round of output, since they are unmodified by the XOR operation. * Thus, since only 31 bits were affected, we can perform a XOR by a right shifted * value again to work our way back to the first line. Repeat this line of thought * once more, and we have obtained a candidate argument into mix32. * Once we have reached this point, we must check our guess. * With an oracle, we can simply query the oracle after each guess. This allows for * a single-output inversion. * Without an oracle, we require more checks in order to find certainty. * In order to check, we simply add gamma to our guess and pass it to mix32. If it * produces the second output, we have either found our seed or one of the small * number of possible collisions. If we have a third output, we add 2*GAMMA to our * guess and pass it to mix32 to compare with it, which should practically, if not * completely, eliminate the chance for collision. Thus, the stream is recovered, * and can be fully enumerated both backwards and forwards by subtracting and * adding gamma, respectively. * * Built/tested on x86_64-pc-linux-gnu (Void Linux) using GCC 14.2.1. */ const uint64_t GAMMA = 0x9e3779b97f4a7c15; inline uint32_t mix32(uint64_t num) { num = (num ^ (num >> 33)) * 0xff51afd7ed558ccd; return static_cast(((num ^ (num >> 33)) * 0xc4ceb9fe1a85ec53) >> 32); } int main() { uint64_t threadSeed = 0x1234567812345678; uint32_t T1 = mix32(threadSeed + GAMMA); uint32_t T2 = mix32(threadSeed + 2*GAMMA); uint32_t T3 = mix32(threadSeed + 3*GAMMA); uint64_t hi = static_cast(T1) << 32; uint64_t recoveredSeed = 0; for (size_t i = 0; i < (1ull << 32); ++i) { uint64_t num2 = hi | static_cast(i); // concat guess num2 *= 0x9cb4b2f8129337db; // unit inverse num2 = num2 ^ (num2 >> 33); // de-xor num2 *= 0x4f74430c22a54005; // other unit inverse num2 = num2 ^ (num2 >> 33); // de-xor again // double check to prevent collision if (mix32(num2 + GAMMA) == T2 && mix32(num2 + 2*GAMMA) == T3) { recoveredSeed = num2 - GAMMA; std::cout << "[+] Recovered seed." << std::endl; std::cout << "[*] 0x" << std::hex << recoveredSeed << std::endl; } // progress check if (i % 10000000 == 0) { std::cout << "[~] " << (static_cast(i) / (1ull << 32)) * 100 << "% Done." << std::endl; } } std::cout << "[+] 100.000% Done" << std::endl; std::cout << "Found seed: 0x" << std::hex << recoveredSeed << std::endl; return 0; }